在學習資料型別的同時,也需要了解資料如何在記憶體裏被存放。如果要深入探討這課題,其實也可以延伸出一些爭議點,例如到底JavaScript是不是只有傳值?還是也有傳址?但這篇文章不會跳這個深坑,畢竟基本功要先打好!!(很會找藉口
以下這張圖簡單說明資料存放在記憶體時的情況。因為物件型別的值可以被修改,如果一併放在棧內存(stack)就會降低效能,所以棧內存只會存放該物件型別值的地址,並引用該地址來指向該物件。
當一個變數被賦予基本型別的值時,整個值就會存在記憶體裏。當我們要拷貝基本型別的值到另一個變數,我們只會拷貝它們的值,而該兩個變數並不會影響到對方。這個情況稱為傳值(pass by value)。
看看以下例子:
var box1 = 10;
var box2 = 'hello';
//拷貝box1,box2的值
var boxA = box1;
var boxB = box2;
boxA = 30;
boxB = 'goodbye';
console.log(box1,box2,boxA,boxB) // 10,"hello",30,"goodbye"
下圖由左至右開始看,解釋了這段程式碼背後的動作。一開始boxA
和boxB
只是各拷貝了box1
和box2
的值,boxA
和box1
,以及box2
和boxB
之間是沒有關係,所以當我要重新賦予新的值給boxA
和boxB
時,box1
和box2
不會被影響。
但物件型別的存取方法就不同了。當變數被賦予的是物件型別的資料,記憶體會存放該物件在記憶體中的地址,並引用該地址來指向該物件。
具體來說,請再看看此文第一張圖,如果變數的值是物件型別,棧內存(stack)會存放該物件在堆內存(heap)的地址,並引用該地址來指向該存放在堆內存(heap)的物件。
所以,當我們要拷貝一個物件到另一個變數時,我們拷貝的是該物件的地址,換言之,如果物件有被修改,所有引用該物件地址的變數,它們的值都會被修改。
如以下例子:
var user = {
name: 'Mary',
age: 30
}
//拷貝user物件的地址
var userCopy = user;
userCopy.age = 20;
console.log(user);
console.log(userCopy);
console:
比較一下:
具體的圖解:
但如果例子是這樣:
var user = {
name: 'Mary',
age: 30
}
var userCopy = user;
userCopy = {
name: 'Mary',
age: 20
};
console.log(user);
console.log(userCopy);
console:
比較一下:
這是因為useCopy
已經被重新賦予一個新的物件,並非修改該物件,所以userCopy
的地址整個變了,並指向另一個新的物件。
當我們把基本型別資料,當成參數傳入函式時,函式的參數會拷貝了那些基本型別的值,所以在函式外的變數並不會被影響。
var box1 = 100;
var box2 = 200;
function add(a,b){
a = 10;
b = 20;
}
add(box1,box2);
console.log(box1,box2) //100,200
以上的例子中,像之前提及的傳值概念一樣,a
和b
拷貝了box1
和box2
的值。即使修改a
和b
的值,box1
和box2
都不會被修改。如下圖:
如果把物件型別傳入函式呢?
var user = {
name: 'Mary',
age: 30
}
function change(obj){
obj.name = 'Peter'
}
change(user);
console.log(user) //{name: "Peter", age: 30}
以上例子中,就如同之前提及傳址的概念一樣,這裏的obj
拷貝了user
的地址,所以當obj
被修改了,user
也會一併被修改。如下圖:
雖然這個情況與傳址的概念是一樣,但是如果在函式裏的obj
被重新賦予一個新的物件時,奇怪的是user
並不會被改變。
var user = {
name: 'Mary',
age: 30
}
function change(obj){
obj = {
name: 'Peter',
age: 20
}
}
change(user);
console.log(user) //{name: "Mary", age: 30}
針對這個情況,有些開發者主張用pass by sharing去形容這個過程會更精準。sharing有「共享」的意思,在這個例子中,意思就是obj
和user
共用一個物件,就像我和你一起開一個google document,如果你修改了我寫的東西,我這邊也會看到。回到這個例子,如果在函式裏的obj
被修改,user
也會被修改。
雖然聽起來與傳址這個概念好像,但如果用pass by sharing去形容,就會更清說明「如果在函式裏的參數被重新賦值,該參數就會指向另一個新的值,函式外的變數不會被改動」的這個情況。
從上面提及的概念中,可以看見如果我們把物件當作參數,傳遞到函式裏時,我們可以一併修改在函式內和外的物件,這樣的函式可以被稱為Impure function,意思是這個函式會汙染到函式外的變數。例如之前曾經提及的這個例子:
function impureFunc(person){
person.age = 20;
return person;
}
var mary = {
name: 'Mary',
age: 30
}
var maryChanged = impureFunc(mary);
console.log(mary); //{name: "Mary", age: 20}
console.log(maryChanged); //{name: "Mary", age: 20}
那麼Pure function呢?就是指即使不會汙染函式外的變數,例如以下例子:
function pureFunc(person){
var newPerson = JSON.parse(JSON.stringify(person));
newPerson.age = 20;
return newPerson
}
var mary = {
name: 'Mary',
age: 30
}
var maryChanged = pureFunc(mary);
console.log(mary); //{name: "Mary", age: 30}
console.log(maryChanged); //{name: "Mary", age: 20}
把傳進來的物件轉成字串再轉成物件,從而建立新的物件,並放在newPerson
這個變數中,再在那個新建的物件裏把age
改成20,因此不會影響到變數mary
的物件。但注意這個方法也會有一些限制,例如如果物件裏有函式或undefined
,在轉換後它們會消失不見。
有些語法,例如Array.map
和Array.filter
,它們的原意就是跟Pure function一樣,因為不想修改到原本的陣列,所以這些語法會拿原本的陣列去做運算或處理,並產生出一個新陣列,而原本的陣列是不會被修改的。例如下面Array.map
的例子:
var user = [
{name: 'Mary', age: 30},
{name: 'Jack', age: 20}
]
var userChanged = user.map(function(person){
var newPerson = {
name: person.name,
age: person.age + 10
};
return newPerson
})
console.log(userChanged)
console.log(user)
Explaining Value vs. Reference in Javascript
深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?
JS 原力覺醒 Day12- 傳值呼叫、傳址呼叫
https://ithelp.ithome.com.tw/articles/10221506
重新認識 JavaScript: Day 05 JavaScript 是「傳值」或「傳址」?
Professional JavaScript for Web Developers
如果改成這樣,就會不一樣了
var user = {
name: 'Mary',
age: 30
}
function change(obj){
obj = {
name: 'Peter',
age: 20
}
return obj;
}
change(user);
console.log(change(user))//{name: 'Peter', age: 20}